export default class WebGL {
  public gl: WebGLRenderingContext;
  public program: WebGLProgram;

  constructor(canvas: HTMLCanvasElement, config?: (_gl: WebGLRenderingContext) => void) {
    let gl = canvas.getContext('webgl');
    this.gl = gl;
    (config instanceof Function) && config(gl);
  }

  private shader(type: GLenum, source: string): WebGLShader | void {
    const { gl } = this;
    const s: WebGLShader = gl.createShader(type);
    gl.shaderSource(s, source);
    gl.compileShader(s);

    if ($log && !gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
      console.log('Failed to compile shaders', gl.getShaderInfoLog(s));
      gl.deleteShader(s);
      return null;
    }
    return s;
  }

  private link(vs: string, fs: string): WebGLProgram | void {
    const { gl } = this;
    const vertex = this.shader(gl.VERTEX_SHADER, vs);
    const fragment = this.shader(gl.FRAGMENT_SHADER, fs);

    if (!!vertex && !!fragment) {
      const program: WebGLProgram = gl.createProgram();
      gl.attachShader(program, vertex);
      gl.attachShader(program, fragment);
      gl.linkProgram(program);
      return program;
    }
    return null;
  }

  public initBuffers(
    { attribute, array, type, size }: {
      attribute: string,
      array: number[],
      size: GLint,
      type: GLenum
    }): boolean {
    const { gl, program } = this;
    const buffer = gl.createBuffer();
    if ($log && !buffer) {
      console.log('Failed to create the buffer object.');
      return false;
    }

    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(array), gl.STATIC_DRAW);
    const attrib = gl.getAttribLocation(program, attribute);
    if ($log && attrib < 0) {
      console.log(`Failed to get the storage location of ${attribute}`);
      return false;
    }
    gl.vertexAttribPointer(attrib, size, type, false, 0, 0);
    gl.enableVertexAttribArray(attrib);
    return true;
  }

  public buffers(index_array: number[],
                 fn: (gl: WebGLRenderingContext,
                      index_array: ArrayLike<number> | ArrayBufferLike,
                      program: WebGLProgram) => void): void {
    const { gl, program } = this;
    let indexBuffer: WebGLBuffer = gl.createBuffer();
    if ($log && !indexBuffer) {
      console.log('Failed to create the EBO');
      return;
    }
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    fn(gl, index_array, program);
  }

  public textures(image: any, fn: (gl: WebGLRenderingContext,
                                   program: WebGLProgram,
                                   texture: WebGLTexture,
                                   img: any) => void): void {
    const { gl, program } = this;
    const texture = gl.createTexture();
    if ($log && !texture) console.log("Failed to create the texture object");
    fn(gl, texture, program, image);
  }

  public initialize(vs: string, fs: string): void {
    const program = this.link(vs, fs);
    if (!!program) {
      this.program = program as WebGLProgram;
      this.gl.useProgram(this.program);
    }
  }
}
